//Copyright (c) 2012 Tilo Szepan, Immo Wache
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
//documentation files (the "Software"), to deal in the Software without restriction, including without limitation
//the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
//and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

//The above copyright notice and this permission notice shall be included in all copies or substantial portions 
//of the Software.

//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
//TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
//THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
//CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
//DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.IO;
using System.Text;
using System.ComponentModel;
using System.Threading;
using System.Globalization;

namespace DuinoClient
{
    public class HttpClient : System.Net.WebClient
    {
        public Action<object> Result;
        public Action<Exception> Exceptions;
        public Action StepPerformed;
        Action<object> _continue;

        public HttpClient()
        {
            Encoding = Encoding.UTF8;
        }

        public void DirList(string uri)
        {
            try
            {
                _continue = (obj) =>
                {
                    _continue = null;
                    var res = ToDirInfo(obj as string);
                    if (Result != null) Result(res);
                };
                DownloadStringAsync(new Uri(uri));
            }
            catch (Exception ex)
            {
                if (Exceptions != null) Exceptions(ex);
            }
        }

        public void CreateFolder(string name)
        {
            string tmpName = name + (name.Last() != '/' ? "/" : null) + "newFold.tmp";
            _continue = (obj) =>
            {
                UploadString(tmpName, "DELETE", "");
                _continue = null;
                if (Result != null) Result(name);
            };
            try
            {
                UploadDataAsync(new Uri(tmpName), "PUT", new byte[0]);
            }
            catch (Exception ex)
            {
                if (Exceptions != null) Exceptions(ex);
            }
        }

        public void RenameItem(string fullName, string newName)
        {
            try
            {
                _continue = (obj) =>
                {
                    _continue = null;
                    if (Result != null) Result(newName);
                };
                UploadStringAsync(new Uri(fullName), "MOVE", newName);
            }
            catch (Exception ex)
            {
                if (Exceptions != null) Exceptions(ex);
            }
        }

        protected override void OnDownloadStringCompleted(DownloadStringCompletedEventArgs e)
        {
            base.OnDownloadStringCompleted(e);
            if (CheckArgs(e)) AsyncCompleted(e.Result);
        }
        protected override void OnDownloadDataCompleted(DownloadDataCompletedEventArgs e)
        {
            base.OnDownloadDataCompleted(e);
            if (CheckArgs(e)) AsyncCompleted(e.Result);
        }
        protected override void OnUploadStringCompleted(UploadStringCompletedEventArgs e)
        {
            base.OnUploadStringCompleted(e);
            if (CheckArgs(e)) AsyncCompleted(e.Result);
        }
        protected override void OnUploadDataCompleted(UploadDataCompletedEventArgs e)
        {
            base.OnUploadDataCompleted(e);
            if (CheckArgs(e)) AsyncCompleted(e.Result);
        }
        bool CheckArgs(AsyncCompletedEventArgs e)
        {
            if (e.Error != null || e.Cancelled)
            {
                ParamStack.Clear();
                if (Exceptions != null) Exceptions(e.Error);
                return false;
            }
            return true;
        }
        void AsyncCompleted(object res)
        {
            if (_continue != null)
            {
                _continue(res);
            }
        }
        protected virtual List<FileSystemItem> ToDirInfo(string cont)
        {
            // ...>date size|dir  <a href="path">name</a><br> || ...><a href="/.../">[To ...]</a><br>
            int end = cont.IndexOf("<a href"), beg;
            var info = new List<FileSystemItem>();

            while (end > 0)
            {
                beg = cont.LastIndexOf('>', end) + 1;
                string val = end > beg ? cont.Substring(beg, end - beg).Trim() : null;
                if (!String.IsNullOrEmpty(val))
                {
                    beg = cont.IndexOf('>', end + 7) + 1;
                    end = cont.IndexOf('<', beg);
                    var href = end > beg ? cont.Substring(beg, end - beg) : null;
                    if (!String.IsNullOrEmpty(href))
                    {
                        href = Uri.UnescapeDataString(href);
                        bool dir = val.Contains("dir");
                        int idx = val.LastIndexOf(' ');
                        if (idx > 0)
                        {
                            var date = val.Substring(0, idx);
                            var sz = dir ? null : val.Substring(idx + 1) + " B";
                            var itm = dir ? FileSystemItem.Directory(href, date) : FileSystemItem.File(href, date, sz);
                            info.Add(itm);
                        }
                    }
                }
                end = cont.IndexOf("<a href", end + 4);
            }
            return info;
        }
        bool IsDir(string path) { return path.Last() == '/' || path.Last() == '\\'; }
        string NameFromPath(string path)
        {
            if (String.IsNullOrEmpty(path)) return "";
            var c = path.Last();
            if (c == '/' || c == '\\') path = path.TrimEnd(c);
            return path.Substring(path.LastIndexOfAny(new[] { '/', '\\' }) + 1);
        }


        int _index;
        List<RemoteArgs> ParamStack = new List<RemoteArgs>();

        public void DownloadItems(StringCollection files, string toDir)
        {
            if (files.Count == 0) return;
            _index = 0;
            if (toDir.Last() != Path.DirectorySeparatorChar) toDir += Path.DirectorySeparatorChar;
            _continue = (obj) =>
            {
                _downloadItems(files, toDir, obj);
            };
            string it0 = files[0];
            try
            {
                if (IsDir(it0))
                    DownloadStringAsync(new Uri(it0));
                else DownloadDataAsync(new Uri(it0));
            }
            catch (Exception ex)
            {
                _continue = null;
                if (Exceptions != null) Exceptions(ex);
            }
        }
        void _downloadItems(StringCollection files, string toDir, object res)
        {
            if (_downloadFolder(res)) return;// null;
            if (StepPerformed != null) StepPerformed();
            var file = files[_index];
            string dest = toDir + NameFromPath(file);

            try
            {
                if (IsDir(file))
                {
                    if (res is string)
                    {
                        var items = ToDirInfo(res as string);
                        ParamStack.Add(new RemoteArgs(file, dest + Path.DirectorySeparatorChar, items));
                        if (!Directory.Exists(dest)) Directory.CreateDirectory(dest);
                        if(_downloadFolder(null)) return;// null;
                    }
                }
                else if (res is byte[])
                {
                    File.WriteAllBytes(dest, res as byte[]);
                }
                if (++_index < files.Count)
                {
                    file = files[_index];
                    if (IsDir(file))
                        DownloadStringAsync(new Uri(file));
                    else DownloadDataAsync(new Uri(file));
                }
                else
                {
                    _continue = null;
                    if (Result != null) Result(files);
                }
            }
            catch (Exception ex)
            {
                _continue = null;
                if (Exceptions != null) Exceptions(ex);
            }
        }
        bool _downloadFolder(object res)
        {
            if (ParamStack.Count == 0) return false;
            var param = ParamStack.Last();
            if (param.Index >= param.Content.Count)
            {
                ParamStack.Remove(param);
                return _downloadFolder(res);
            }
            var item = param.Content[param.Index];
            if (item.IsDir && res is String)
            {
                var cont = ToDirInfo(res as String);
                var newParam = new RemoteArgs(param.Source + item.Name + '/', param.Destiny + item.Name + Path.DirectorySeparatorChar, cont);
                if (!Directory.Exists(newParam.Destiny)) Directory.CreateDirectory(newParam.Destiny);
                param.Index++;
                ParamStack.Add(newParam);
                return _downloadFolder(null);
            }
            else if (res is byte[])
            {
                File.WriteAllBytes(param.Destiny + item.Name, res as byte[]);
                param.Index++;
                return _downloadFolder(null);
            }
            if (item.IsDir)
                DownloadStringAsync(new Uri(param.Source + item.Name));
            else
                DownloadDataAsync(new Uri(param.Source + item.Name));

            return true;
        }

        public void UploadItems(StringCollection files, string uri)
        {
            if (files.Count == 0) return;
            if (uri.Last() != '/') uri += '/';
            _index = 0;
            _continue = (obj) =>
            {
                _uploadItems(files, uri, obj);
            };
            _uploadItems(files, uri, null);
        }
        void _uploadItems(StringCollection files, string uri, object res)
        {
            if (_uploadFolder(res)) return;
            if (StepPerformed != null) StepPerformed();
GoOn:
            if (_index < files.Count)
            {
                try
                {
                    string file = files[_index ++];
                    string dest = uri + NameFromPath(file);
                    if (Directory.Exists(file))
                    {
                        var di = new DirectoryInfo(file);
                        var arg = new RemoteArgs(file, dest + '/', di.GetFileSystemInfos());
                        ParamStack.Add(arg);
                        if (!_uploadFolder(null)) goto GoOn;
                    }
                    else
                    {
                        var dat = File.ReadAllBytes(file);
                        UploadDataAsync(new Uri(dest), "PUT", dat);
                    }
                }
                catch (Exception ex)
                {
                    _continue = null;
                    if (Exceptions != null) Exceptions(ex);
                }
            }
            else
            {
                _continue = null;
                if (Result != null) Result(files);
            }
        }
        bool _uploadFolder(object res)
        {
            if (ParamStack.Count == 0) return false;
            var param = ParamStack.Last();
            if (param.Index >= param.UpContent.Count)
            {
                if (param.UpContent.Count == 0)
                {
                    if (res == null)
                        UploadDataAsync(new Uri(param.Destiny + "/TMPFILE.TXT"), "PUT", new byte[0]);
                    else
                    {
                        ParamStack.Remove(param);
                        UploadStringAsync(new Uri(param.Destiny + "/TMPFILE.TXT"), "DELETE", "");
                    }
                    return true;
                }
                ParamStack.Remove(param);
                return _uploadFolder(res);
            }

            try
            {
                var file = param.UpContent[param.Index ++];
                string dest = param.Destiny + file.Name;
                if (file is DirectoryInfo)
                {
                    var arg = new RemoteArgs(file.FullName, dest + '/', (file as DirectoryInfo).GetFileSystemInfos());
                    ParamStack.Add(arg);
                    return _uploadFolder(null);
                }
                else
                {
                    var dat = File.ReadAllBytes(file.FullName);
                    UploadDataAsync(new Uri(dest), "PUT", dat);
                }
            }
            catch (Exception ex)
            {
                _continue = null;
                if (Exceptions != null) Exceptions(ex);
            }
            return true;
        }

        public void DeleteItems(StringCollection files)
        {
            if (files.Count == 0) return;
            _index = 0;
            _continue = (obj) =>
                {
                    _deleteItems(files, obj);
                };
            _deleteItems(files, null);
        }
        bool _skipResult = false;
        void _deleteItems(StringCollection files, object res)
        {
            if (_skipResult)
            {
                _skipResult = false;
                res = null;
            }
            if (_deleteFolder(res)) return;
            if (_index < files.Count)
            {
                try
                {
                    var itm = files[_index];
                    if (IsDir(itm))
                    {
                        if (res != null)
                        {
                            _index++;
                            var cont = ToDirInfo(res as String);
                            var arg = new RemoteArgs(itm, null, cont);
                            ParamStack.Add(arg);
                            if (_deleteFolder(null)) return;
                        }
                        else
                            DownloadStringAsync(new Uri(itm));
                    }
                    else
                    {
                        _index++;
                        _skipResult = true;
                        UploadStringAsync(new Uri(itm), "DELETE", "");
                    }
                }
                catch (Exception ex)
                {
                    _continue = null;
                    if (Exceptions != null) Exceptions(ex);
                }
            }
            else
            {
                _continue = null;
                if (Result != null) Result(files);
            }
        }
        bool _deleteFolder(object res)
        {
            if (ParamStack.Count == 0) return false;
            var param = ParamStack.Last();
            if (param.Index >= param.Content.Count)
            {
                ParamStack.Remove(param);
                _skipResult = true;
                UploadStringAsync(new Uri(param.Source), "DELETE", "");
                return true;
            }
            try
            {
                var itm = param.Content[param.Index];
                if (itm.IsDir)
                {
                    if (res != null)// && !IsDeleteResponce(res as String))
                    {
                        param.Index++;
                        var cont = ToDirInfo(res as String);
                        var arg = new RemoteArgs(param.Source + itm.Name + '/', null, cont);
                        ParamStack.Add(arg);
                        return _deleteFolder(null);
                    }
                    else
                        DownloadStringAsync(new Uri(param.Source + itm.Name));
                }
                else
                {
                    param.Index++;
                    _skipResult = true;
                    UploadStringAsync(new Uri(param.Source + itm.Name), "DELETE", "");
                }
            }
            catch (Exception ex)
            {
                _continue = null;
                if (Exceptions != null) Exceptions(ex);
            }
            return true;
        }

        class RemoteArgs
        {
            public string Source;
            public string Destiny;
            public List<FileSystemItem> Content;
            public List<FileSystemInfo> UpContent;
            public int Index;
            public RemoteArgs(string src, string dest, List<FileSystemItem> cont)
            {
                Source = src; Destiny = dest; Content = cont; Index = 0;
            }
            public RemoteArgs(string src, string dest, FileSystemInfo[] cont)
            {
                Source = src; Destiny = dest; Index = 0;
                UpContent = new List<FileSystemInfo>(cont);
            }
        }
    }
}

